<html lang="en">
	<head>
		<title>three.js - WebGPU - Compute</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
		<!-- WebGPU (For Chrome 94-97), expires 10/11/2021 -->
		<meta http-equiv="origin-trial" content="Agfr4hEaaoH1kaTtGTZY4OU2lQQOv+gWq+8rYeHZBPWNnLww/smePFCIJOUdRFnQZkO3KAio+SNJapjzaoyFfQQAAABLeyJvcmlnaW4iOiJodHRwczovL3RocmVlanMub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJHUFUiLCJleHBpcnkiOjE2NDMxNTUxOTl9">
	</head>
	<body>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> WebGPU - Compute<br/>(Chrome Canary with flag: --enable-unsafe-webgpu)
		</div>

		<script type="importmap">
		{
			"imports": {
				"three": "../build/three.module.js"
			}
		}
		</script>
		<script type="module">

			import * as THREE from 'three';

			import { GUI } from './jsm/libs/dat.gui.module.js';

			import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js';
			import WebGPU from './jsm/renderers/webgpu/WebGPU.js';

			import WebGPUStorageBuffer from './jsm/renderers/webgpu/WebGPUStorageBuffer.js';
			import WebGPUUniformBuffer from './jsm/renderers/webgpu/WebGPUUniformBuffer.js';
			import * as WebGPUBufferUtils from './jsm/renderers/webgpu/WebGPUBufferUtils.js';
			import WebGPUUniformsGroup from './jsm/renderers/webgpu/WebGPUUniformsGroup.js';
			import { Vector2Uniform } from './jsm/renderers/webgpu/WebGPUUniform.js';

			import * as Nodes from './jsm/renderers/nodes/Nodes.js';

			let camera, scene, renderer;
			let pointer;
			let scaleUniformBuffer;
			let scaleVector = new THREE.Vector3( 1, 1, 1 );

			const computeParams = [];

			init().then( animate ).catch( error );

			async function init() {

				if ( WebGPU.isAvailable() === false ) {

					document.body.appendChild( WebGPU.getErrorMessage() );

					throw 'No WebGPU support';

				}

				camera = new THREE.OrthographicCamera( - 1.0, 1.0, 1.0, - 1.0, 0, 1 );
				camera.position.z = 1;

				scene = new THREE.Scene();
				scene.background = new THREE.Color( 0x000000 );

				const particleNum = 65000; // 16-bit limit
				const particleSize = 4; // 16-byte stride align

				const particleArray = new Float32Array( particleNum * particleSize );
				const velocityArray = new Float32Array( particleNum * particleSize );

				for ( let i = 0; i < particleArray.length; i += particleSize ) {

					const r = Math.random() * 0.01 + 0.0005;
					const degree = Math.random() * 360;
					velocityArray[ i + 0 ] = r * Math.sin( degree * Math.PI / 180 );
					velocityArray[ i + 1 ] = r * Math.cos( degree * Math.PI / 180 );

				}

				const particleBuffer = new WebGPUStorageBuffer( 'particle', new THREE.BufferAttribute( particleArray, particleSize ) );
				const velocityBuffer = new WebGPUStorageBuffer( 'velocity', new THREE.BufferAttribute( velocityArray, particleSize ) );

				const scaleUniformLength = WebGPUBufferUtils.getVectorLength( 2, 3 ); // two vector3 for array

				scaleUniformBuffer = new WebGPUUniformBuffer( 'scaleUniform', new Float32Array( scaleUniformLength ) );

				pointer = new THREE.Vector2( - 10.0, - 10.0 ); // Out of bounds first

				const pointerGroup = new WebGPUUniformsGroup( 'mouseUniforms' ).addUniform(
					new Vector2Uniform( 'pointer', pointer )
				);

				// Object keys need follow the binding shader sequence

				const computeBindings = [
					particleBuffer,
					velocityBuffer,
					scaleUniformBuffer,
					pointerGroup
				];

				const computeShader = `

					//
					// Buffer
					//

					[[ block ]]
					struct Particle {
						value : array< vec4<f32> >;
					};
					[[ binding( 0 ), group( 0 ) ]]
					var<storage,read_write> particle : Particle;

					[[ block ]]
					struct Velocity {
						value : array< vec4<f32> >;
					};
					[[ binding( 1 ), group( 0 ) ]]
					var<storage,read_write> velocity : Velocity;

					//
					// Uniforms
					//

					[[ block ]]
					struct Scale {
						value : array< vec3<f32>, 2 >;
					};
					[[ binding( 2 ), group( 0 ) ]]
					var<uniform> scaleUniform : Scale;

					[[block]]
					struct MouseUniforms {
						pointer : vec2<f32>;
					};
					[[ binding( 3 ), group( 0 ) ]]
					var<uniform> mouseUniforms : MouseUniforms;

					[[ stage( compute ), workgroup_size( 64 ) ]]
					fn main( [[builtin(global_invocation_id)]] id : vec3<u32> ) {

						// get particle index

						let index : u32 = id.x * 3u;

						// update speed

						var position : vec4<f32> = particle.value[ index ] + velocity.value[ index ];

						// update limit

						let limit : vec2<f32> = scaleUniform.value[ 0 ].xy;

						if ( abs( position.x ) >= limit.x ) {

							if ( position.x > 0.0 ) {

								position.x = limit.x;

							} else {

								position.x = -limit.x;

							}

							velocity.value[ index ].x = - velocity.value[ index ].x;

						}

						if ( abs( position.y ) >= limit.y ) {

							if ( position.y > 0.0 ) {

								position.y = limit.y;

							} else {

								position.y = -limit.y;

							}

							velocity.value[ index ].y = - velocity.value[ index ].y;

						}

						// update mouse

						let POINTER_SIZE : f32 = .1;

						let dx : f32 = mouseUniforms.pointer.x - position.x;
						let dy : f32 = mouseUniforms.pointer.y - position.y;
						let distanceFromPointer : f32 = sqrt( dx * dx + dy * dy );

						if ( distanceFromPointer <= POINTER_SIZE ) {

							position.x = 0.0;
							position.y = 0.0;
							position.z = 0.0;

						}

						// update buffer

						particle.value[ index ] = position;

					}

				`;

				computeParams.push( {
					num: particleNum,
					shader: computeShader,
					bindings: computeBindings
				} );

				// Use a compute shader to animate the point cloud's vertex data.

				const pointsGeometry = new THREE.BufferGeometry().setAttribute(
					'position', particleBuffer.attribute
				);

				const pointsMaterial = new Nodes.PointsNodeMaterial();
				pointsMaterial.colorNode = new Nodes.OperatorNode( '+', new Nodes.PositionNode(), new Nodes.ColorNode( new THREE.Color( 0xFFFFFF ) ) );

				const mesh = new THREE.Points( pointsGeometry, pointsMaterial );
				scene.add( mesh );

				renderer = new WebGPURenderer();
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				document.body.appendChild( renderer.domElement );

				window.addEventListener( 'resize', onWindowResize );
				window.addEventListener( 'mousemove', onMouseMove );

				// gui

				const gui = new GUI();

				gui.add( scaleVector, 'x', 0, 1, 0.01 );
				gui.add( scaleVector, 'y', 0, 1, 0.01 );

				return renderer.init();

			}

			function onWindowResize() {

				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function onMouseMove( event ) {

				const x = event.clientX;
				const y = event.clientY;

				const width = window.innerWidth;
				const height = window.innerHeight;

				pointer.set(
					( x / width - 0.5 ) * 2.0,
					( - y / height + 0.5 ) * 2.0
				);

			}

			function animate() {

				requestAnimationFrame( animate );

				renderer.compute( computeParams );
				renderer.render( scene, camera );

				scaleVector.toArray( scaleUniformBuffer.buffer, 0 );

			}

			function error( error ) {

				console.error( error );

			}

		</script>
	</body>
</html>
